﻿using DeepZoomPivotConstructor.DTBuilderLib;
using DeepZoomPivotConstructor.ErrorAndLoggingInterfaces;
using DeepZoomPivotConstructor.PivotLib.PivotSchema;
using DeepZoomPivotConstructor.VisualCacheInterfaces;
using DeepZoomPivotConstructor.VisualLib;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Serialization;

namespace DeepZoomPivotConstructor.PivotLib
{
    /// <summary>
    /// Allows one to process images (etc.) into a pivot collection.
    /// </summary>
    public class PivotProcessor
    {
        private IErrorLogger Logger { get; set; }

        /// <summary>
        /// Create a new pivot processor
        /// </summary>
        /// <param name="logger"></param>
        public PivotProcessor(IErrorLogger logger, IVisualCacheDB cache = null)
        {
            Logger = logger;
            _cache = cache;
        }

        /// <summary>
        /// The cache we use to store results.
        /// </summary>
        IVisualCacheDB _cache = null;

        /// <summary>
        /// Keep track of all the classes we've seen plots put into
        /// </summary>
        private HashSet<string> _classes_seen = new HashSet<string>();

        /// <summary>
        /// Given a collection and output directory, create the pivot collection,
        /// including running all of the DZ and turning them into a collection.
        /// </summary>
        /// <param name="pivotImageCollection">Collection of images to render</param>
        /// <param name="classifier">Optional functional argument to classify every image. Allows sub-cxml files to be written quickly.</param>
        /// <param name="collectionName">Name to be given to the collection (stored in the cxml file)</param>
        /// <param name="noRendering">Set to true if no rendering should occur. Useful if only facet information was updated.</param>
        /// <param name="outputPivotDirectory">Where the pivot output should be written.</param>
        /// <returns>The cxml file that can be loaded in a pivot application.</returns>
        public async Task<FileInfo> CreatePivotCollection(PivotCollection pivotImageCollection,
            DirectoryInfo outputPivotDirectory,
            string collectionName,
            Func<PivotImage, string[]> classifier = null)
        {
            ///
            /// Create the master collection
            /// 

            var pCollection = new PivotSchema.Collection();
            pCollection.Name = collectionName;
            pCollection.SchemaVersion = (decimal)1.0;

            pCollection.Items = new PivotSchema.itemList[1];
            pCollection.Items[0] = new PivotSchema.itemList();
            VisualHolder masterCollectionVisual = new VisualHolder();
            Dictionary<string, facetType> fTable = new Dictionary<string, facetType>();
            pCollection.Items[0].Item = (from img in pivotImageCollection select ConvertToPivotImage(img, masterCollectionVisual, fTable)).ToArray();

            ///
            /// Render the main dude
            /// 

            DirectoryInfo dzImage = new DirectoryInfo(outputPivotDirectory.FullName + "\\dz");
            DeepZoomBuilder dzb = new DeepZoomBuilder(Logger, _cache);
            var imageIndexing = (await dzb.RenderVisual(dzImage, masterCollectionVisual)).ToArray();
            pCollection.Items[0].ImgBase = "dz\\dz.xml";

            //
            // The numbering may not be sequential. This happens when an image is removed from a collection that
            // is being re-rendered.
            // 

            if (imageIndexing.Length != pCollection.Items[0].Item.Length)
                throw new InvalidOperationException("Pictures and rendered items not the same!");

            foreach (var p in imageIndexing.Zip(Enumerable.Range(0, pCollection.Items[0].Item.Length), (idx, cnt) => Tuple.Create(idx, cnt)))
            {
                pCollection.Items[0].Item[p.Item2].Img = "#" + p.Item1.ToString();
                pCollection.Items[0].Item[p.Item2].Id = p.Item1.ToString();
            }

            ///
            /// Do the facet catagories. First this means finding all the catagories we know about!
            /// 

            var facNames = from allitems in pCollection.Items[0].Item
                           from f in allitems.Facets.Facet
                           group f by f.Name.ToUpper();
            var allFacCat = (from fn in facNames
                             let key = fn.First().Name
                             select ConvertToFacCategory(pivotImageCollection, key, fTable[key]))
                             .ToArray();

            pCollection.FacetCategories = new PivotSchema.facetCategoryList();
            pCollection.FacetCategories.FacetCategory = allFacCat;

            ///
            /// Write out the file
            /// 

            FileInfo output = new FileInfo(outputPivotDirectory.FullName + "\\collection.cxml");
            SaveCollectionXML(pCollection, output);

            ///
            /// iF we can, classifiy everything
            /// 

            if (classifier != null)
            {
                var imageClass = pivotImageCollection.Select(pi => classifier(pi));
                var pairedCollectionInfoWithClasses = from dImageInfo in imageClass.Zip(pCollection.Items[0].Item, (s, pvi) => new KeyValuePair<string[], PivotSchema.item>(s, pvi)) select dImageInfo;
                var pairedCollectionInfo = from dseq in pairedCollectionInfoWithClasses
                                           from cls in dseq.Key
                                           select new KeyValuePair<string, PivotSchema.item>(cls, dseq.Value);
                Dictionary<string, List<PivotSchema.item>> foundCollections = new Dictionary<string, List<PivotSchema.item>>();
                foreach (var item in pairedCollectionInfo)
                {
                    if (!foundCollections.ContainsKey(item.Key))
                    {
                        foundCollections[item.Key] = new List<item>();
                        _classes_seen.Add(item.Key);
                    }
                    foundCollections[item.Key].Add(item.Value);
                }

                foreach (var item in foundCollections)
                {
                    PivotSchema.Collection cl = new PivotSchema.Collection();
                    cl.Name = collectionName + " (" + item.Key + ")";
                    cl.SchemaVersion = (decimal)1.0;
                    cl.Items = new itemList[1];
                    cl.Items[0] = new itemList();
                    cl.Items[0].Item = item.Value.ToArray();
                    cl.Items[0].ImgBase = "dz\\dz.xml";
                    cl.FacetCategories = pCollection.FacetCategories;

                    SaveCollectionXML(cl, new FileInfo(outputPivotDirectory.FullName + "\\" + item.Key + ".cxml"));
                }
            }

            return output;
        }

        /// <summary>
        /// Helpe rountine to seralize the XML.
        /// </summary>
        /// <param name="pCollection"></param>
        /// <param name="output"></param>
        private static void SaveCollectionXML(PivotSchema.Collection pCollection, FileInfo output)
        {
            using (var swriter = output.CreateText())
            {
                var ser = new XmlSerializer(typeof(PivotSchema.Collection));
                ser.Serialize(swriter, pCollection);
            }
        }

        /// <summary>
        /// Converts what we know about a facet into a catagory.
        /// </summary>
        /// <param name="c"></param>
        /// <param name="facetName"></param>
        private static PivotSchema.facetCategory ConvertToFacCategory(PivotCollection c, string facetName, facetType fType)
        {
            PivotSchema.facetCategory cat = new PivotSchema.facetCategory();
            var info = c.FacetCatagories[facetName];

            cat.Name = facetName;

            cat.IsFilterVisible = info.IsFilterVisible;
            cat.IsMetaDataVisible = info.IsMetaDataVisible;
            cat.IsWordWheelVisible = info.IsWordWheelVisible;

            cat.IsFilterVisibleSpecified = true;
            cat.IsMetaDataVisibleSpecified = true;
            cat.IsWordWheelVisibleSpecified = true;

            cat.Type = fType;

            if (fType == facetType.Number && !string.IsNullOrWhiteSpace(info.NumberFormat))
            {
                cat.Format = info.NumberFormat;
            }

            return cat;
        }

        /// <summary>
        /// Converts one of our internal image guys into a Pivot image
        /// </summary>
        /// <param name="img"></param>
        /// <returns></returns>
        private PivotSchema.item ConvertToPivotImage(PivotImage img, VisualHolder holder, Dictionary<string, facetType> fTable)
        {
            PivotSchema.item result = new PivotSchema.item();

            result.Description = img.Description;
            result.Name = img.Name;

            ///
            /// Add the main image to the list of images we are going to need to run
            /// 

            holder.AddImage(img.Image);

            ///
            /// Do any facets of data that have been added
            /// 

            var byName = from f in img
                         group f by f.Name.ToUpper();
            var facets = (from fs in byName select ConvertToFacet(fs.First().Name, fs, fTable)).ToArray();
            result.Facets = new PivotSchema.facetList();
            result.Facets.Facet = facets;

            return result;
        }

        /// <summary>
        /// Looks at all the facets registered under a particular name, and constructs
        /// the final results for that facet.
        /// </summary>
        /// <param name="facetName"></param>
        /// <param name="mdList"></param>
        /// <returns></returns>
        private PivotSchema.facet ConvertToFacet(string facetName, IEnumerable<PivotMetadataBase> mdList, Dictionary<string, facetType> fTable)
        {
            PivotSchema.facet result = new PivotSchema.facet();

            result.Name = facetName;

            ///
            /// Convert the list, but make sure they are all of the same type
            /// 

            var converted = (from f in mdList select f.ToFacetInfo()).ToArray();

            var typeOfFacet = converted[0].GetType();
            if (!converted.All(f => f.GetType() == typeOfFacet))
            {
                throw new InvalidOperationException("All facet types with same name must be the same type (facet name was '" + facetName + "'");
            }

            ///
            /// Convert to the "right" type...
            /// 

            if (typeOfFacet == typeof(PivotSchema.linkType))
            {
                result.Link = (from f in converted select (PivotSchema.linkType)f).ToArray();
                fTable[facetName] = facetType.Link;
            }
            else if (typeOfFacet == typeof(PivotSchema.stringType))
            {
                result.String = (from f in converted select (PivotSchema.stringType)f).ToArray();
                fTable[facetName] = facetType.String;
            }
            else if (typeOfFacet == typeof(PivotSchema.dateTimeType))
            {
                result.DateTime = (from f in converted select (PivotSchema.dateTimeType)f).ToArray();
                fTable[facetName] = facetType.DateTime;
            }
            else if (typeOfFacet == typeof(PivotSchema.numberType))
            {
                result.Number = (from f in converted select (PivotSchema.numberType)f).ToArray();
                fTable[facetName] = facetType.Number;
            }
            return result;
        }

        /// <summary>
        /// Return a list of all the classes that we saw used
        /// </summary>
        public string[] ClassesUsed
        {
            get { return _classes_seen.ToArray(); }
        }
    }
}
